home
***
CD-ROM
|
disk
|
FTP
|
other
***
search
/
PC World Komputer 2010 April
/
PCWorld0410.iso
/
pluginy Firefox
/
1843
/
1843.xpi
/
content
/
firebug
/
spy.js
< prev
next >
Wrap
Text File
|
2010-01-15
|
30KB
|
970 lines
/* See license.txt for terms of usage */
FBL.ns(function() { with (FBL) {
// ************************************************************************************************
// Constants
const Cc = Components.classes;
const Ci = Components.interfaces;
// List of contexts with XHR spy attached.
var contexts = [];
// ************************************************************************************************
// Spy Module
/**
* @module Represents a XHR Spy module. The main purpose of the XHR Spy feature is to monitor
* XHR activity of the current page and create appropriate log into the Console panel.
* This feature can be controlled by an option <i>Show XMLHttpRequests</i> (from within the
* console panel).
*
* The module is responsible for attaching/detaching a HTTP Observers when Firebug is
* activated/deactivated for a site.
*/
Firebug.Spy = extend(Firebug.Module,
/** @lends Firebug.Spy */
{
dispatchName: "spy",
initialize: function()
{
if (Firebug.TraceModule)
Firebug.TraceModule.addListener(this.TraceListener);
Firebug.Module.initialize.apply(this, arguments);
},
shutdown: function()
{
Firebug.Module.shutdown.apply(this, arguments);
if (Firebug.TraceModule)
Firebug.TraceModule.removeListener(this.TraceListener);
},
initContext: function(context)
{
context.spies = [];
if (Firebug.showXMLHttpRequests && Firebug.Console.isAlwaysEnabled())
this.attachObserver(context, context.window);
},
destroyContext: function(context)
{
// For any spies that are in progress, remove our listeners so that they don't leak
this.detachObserver(context, null);
delete context.spies;
},
watchWindow: function(context, win)
{
if (Firebug.showXMLHttpRequests && Firebug.Console.isAlwaysEnabled())
this.attachObserver(context, win);
},
unwatchWindow: function(context, win)
{
try
{
// This make sure that the existing context is properly removed from "contexts" array.
this.detachObserver(context, win);
}
catch (ex)
{
// Get exceptions here sometimes, so let's just ignore them
// since the window is going away anyhow
ERROR(ex);
}
},
updateOption: function(name, value)
{
// XXXjjb Honza, if Console.isEnabled(context) false, then this can't be called,
// but somehow seems not correct
if (name == "showXMLHttpRequests")
{
var tach = value ? this.attachObserver : this.detachObserver;
for (var i = 0; i < TabWatcher.contexts.length; ++i)
{
var context = TabWatcher.contexts[i];
iterateWindows(context.window, function(win)
{
tach.apply(this, [context, win]);
});
}
}
},
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
// Attaching Spy to XHR requests.
/**
* Returns false if Spy should not be attached to XHRs executed by the specified window.
*/
skipSpy: function(win)
{
if (!win)
return true;
// Don't attach spy to chrome.
var uri = safeGetWindowLocation(win);
if (uri && (uri.indexOf("about:") == 0 || uri.indexOf("chrome:") == 0))
return true;
},
attachObserver: function(context, win)
{
if (Firebug.Spy.skipSpy(win))
return;
for (var i=0; i<contexts.length; ++i)
{
if ((contexts[i].context == context) && (contexts[i].win == win))
return;
}
// Register HTTP observers only once.
if (contexts.length == 0)
{
httpObserver.addObserver(SpyHttpObserver, "firebug-http-event", false);
SpyHttpActivityObserver.registerObserver();
}
contexts.push({context: context, win: win});
},
detachObserver: function(context, win)
{
for (var i=0; i<contexts.length; ++i)
{
if (contexts[i].context == context)
{
if (win && (contexts[i].win != win))
continue;
contexts.splice(i, 1);
// If no context is using spy, remvove the (only one) HTTP observer.
if (contexts.length == 0)
{
httpObserver.removeObserver(SpyHttpObserver, "firebug-http-event");
SpyHttpActivityObserver.unregisterObserver();
}
return;
}
}
},
/**
* Return XHR object that is associated with specified request <i>nsIHttpChannel</i>.
* Returns null if the request doesn't represent XHR.
*/
getXHR: function(request)
{
// Does also query-interface for nsIHttpChannel.
if (!(request instanceof Ci.nsIHttpChannel))
return null;
try
{
var callbacks = request.notificationCallbacks;
return (callbacks ? callbacks.getInterface(Ci.nsIXMLHttpRequest) : null);
}
catch (exc)
{
if (exc.name == "NS_NOINTERFACE")
{
}
}
return null;
},
});
// ************************************************************************************************
/**
* @class This observer uses {@link HttpRequestObserver} to monitor start and end of all XHRs.
* using <code>http-on-modify-request</code>, <code>http-on-examine-response</code> and
* <code>http-on-examine-cached-response</code> events. For every monitored XHR a new
* instance of {@link Firebug.Spy.XMLHttpRequestSpy} object is created. This instance is removed
* when the XHR is finished.
*/
var SpyHttpObserver =
/** @lends SpyHttpObserver */
{
observe: function(request, topic, data)
{
try
{
if (topic != "http-on-modify-request" &&
topic != "http-on-examine-response" &&
topic != "http-on-examine-cached-response")
{
return;
}
this.observeRequest(request, topic);
}
catch (exc)
{
}
},
observeRequest: function(request, topic)
{
var win = getWindowForRequest(request);
var xhr = Firebug.Spy.getXHR(request);
// The request must be associated with window (i.e. tab) and it also must be
// real XHR request.
if (!win || !xhr)
return;
for (var i=0; i<contexts.length; ++i)
{
var context = contexts[i];
if (context.win == win)
{
var spyContext = context.context;
var requestName = request.URI.asciiSpec;
var requestMethod = request.requestMethod;
if (topic == "http-on-modify-request")
this.requestStarted(request, xhr, spyContext, requestMethod, requestName);
else if (topic == "http-on-examine-response")
this.requestStopped(request, xhr, spyContext, requestMethod, requestName);
else if (topic == "http-on-examine-cached-response")
this.requestStopped(request, xhr, spyContext, requestMethod, requestName);
return;
}
}
},
requestStarted: function(request, xhr, context, method, url)
{
var spy = getSpyForXHR(request, xhr, context);
spy.method = method;
spy.href = url;
if (method == "POST" || method == "PUT")
spy.postText = readPostTextFromRequest(request, context);
spy.urlParams = parseURLParams(spy.href);
// In case of redirects there is no stack and the source link is null.
spy.sourceLink = getStackSourceLink();
if (!spy.requestHeaders)
spy.requestHeaders = getRequestHeaders(spy);
// If it's enabled log the request into the console tab.
if (Firebug.showXMLHttpRequests && Firebug.Console.isAlwaysEnabled())
{
spy.logRow = Firebug.Console.log(spy, spy.context, "spy", null, true);
setClass(spy.logRow, "loading");
}
// Notify registered listeners. The onStart event is fired once for entire XHR
// (even if there is more redirects within the process).
var name = request.URI.asciiSpec;
var origName = request.originalURI.asciiSpec;
if (name == origName)
dispatch(Firebug.Spy.fbListeners, "onStart", [context, spy]);
// Remember the start time et the end, so it's most accurate.
spy.sendTime = new Date().getTime();
},
requestStopped: function(request, xhr, context, method, url)
{
var spy = getSpyForXHR(request, xhr, context);
if (!spy)
return;
spy.endTime = new Date().getTime();
spy.responseTime = spy.endTime - spy.sendTime;
spy.mimeType = Firebug.NetMonitor.Utils.getMimeType(request.contentType, request.name);
if (!spy.responseHeaders)
spy.responseHeaders = getResponseHeaders(spy);
if (!spy.statusText)
{
try
{
spy.statusCode = request.responseStatus;
spy.statusText = request.responseStatusText;
}
catch (exc)
{
}
}
if (spy.logRow)
{
updateLogRow(spy);
updateHttpSpyInfo(spy);
}
// Remove only the Spy object that has been created for an intermediate rediret
// request. These exist only to be also displayed in the console and they
// don't attach any listeners to the original XHR object (which is always created
// only once even in case of redirects).
// xxxHonza: These requests are not observer by the activityObserver now
// (if they should be observed we have to remove them in the activityObserver)
if (!spy.onLoad && spy.context.spies)
remove(spy.context.spies, spy);
}
};
// ************************************************************************************************
// Activity Observer
/**
* @class This observer is used to properly monitor even mulipart XHRs. It's based on
* an activity-observer component that has been introduced in Firefox 3.6.
*/
var SpyHttpActivityObserver = extend(Firebug.NetMonitor.NetHttpActivityObserver,
/** @lends SpyHttpActivityObserver */
{
activeRequests: [],
observeRequest: function(request, activityType, activitySubtype, timestamp,
extraSizeData, extraStringData)
{
if (activityType != Ci.nsIHttpActivityObserver.ACTIVITY_TYPE_HTTP_TRANSACTION &&
(activityType == Ci.nsIHttpActivityObserver.ACTIVITY_TYPE_SOCKET_TRANSPORT &&
activitySubtype != Ci.nsISocketTransport.STATUS_RECEIVING_FROM))
return;
var win = getWindowForRequest(request);
if (!win)
{
var index = this.activeRequests.indexOf(request);
if (!(win = this.activeRequests[index+1]))
return;
}
for (var i=0; i<contexts.length; ++i)
{
var context = contexts[i];
if (context.win == win)
{
var spyContext = context.context;
var spy = getSpyForXHR(request, null, spyContext, true);
if (spy)
this.observeXHRActivity(win, spy, request, activitySubtype, timestamp);
return;
}
}
},
observeXHRActivity: function(win, spy, request, activitySubtype, timestamp)
{
// Activity observer has precise time info so, use it.
var time = new Date();
time.setTime(timestamp/1000);
if (activitySubtype == Ci.nsIHttpActivityObserver.ACTIVITY_SUBTYPE_REQUEST_HEADER)
{
this.activeRequests.push(request);
this.activeRequests.push(win);
spy.sendTime = time;
spy.transactionStarted = true;
}
else if (activitySubtype == Ci.nsIHttpActivityObserver.ACTIVITY_SUBTYPE_TRANSACTION_CLOSE)
{
var index = this.activeRequests.indexOf(request);
this.activeRequests.splice(index, 2);
spy.endTime = time;
spy.transactionClosed = true;
// This should be the proper time to detach the Spy object, but only
// in the case when the XHR is already loaded. If the XHR is made as part of the
// page load, it may happen that the event (readyState == 4) comes later
// than actual TRANSACTION_CLOSE.
if (spy.loaded)
spy.detach();
}
else if (activitySubtype == Ci.nsISocketTransport.STATUS_RECEIVING_FROM)
{
spy.endTime = time;
}
}
});
// ************************************************************************************************
function getSpyForXHR(request, xhrRequest, context, noCreate)
{
var spy = null;
// Iterate all existing spy objects in this context and look for one that is
// already created for this request.
var length = context.spies.length;
for (var i=0; i<length; i++)
{
spy = context.spies[i];
if (spy.request == request)
return spy;
}
if (noCreate)
return null;
spy = new Firebug.Spy.XMLHttpRequestSpy(request, xhrRequest, context);
context.spies.push(spy);
var name = request.URI.asciiSpec;
var origName = request.originalURI.asciiSpec;
// Attach spy only to the original request. Notice that there can be more network requests
// made by the same XHR if redirects are involved.
if (name == origName)
spy.attach();
return spy;
}
// ************************************************************************************************
/**
* @class This class represents a Spy object that is attached to XHR. This object
* registers various listeners into the XHR in order to monitor various events fired
* during the request process (onLoad, onAbort, etc.)
*/
Firebug.Spy.XMLHttpRequestSpy = function(request, xhrRequest, context)
{
this.request = request;
this.xhrRequest = xhrRequest;
this.context = context;
this.responseText = "";
// For compatibility with the Net templates.
this.isXHR = true;
// Support for activity-observer
this.transactionStarted = false;
this.transactionClosed = false;
};
Firebug.Spy.XMLHttpRequestSpy.prototype =
/** @lends Firebug.Spy.XMLHttpRequestSpy */
{
attach: function()
{
var spy = this;
this.onReadyStateChange = function(event) { onHTTPSpyReadyStateChange(spy, event); };
this.onLoad = function() { onHTTPSpyLoad(spy); };
this.onError = function() { onHTTPSpyError(spy); };
this.onAbort = function() { onHTTPSpyAbort(spy); };
// xxxHonza: #502959 is still failing on Fx 3.5
// Use activity distributor to identify 3.6
if (SpyHttpActivityObserver.getActivityDistributor())
{
this.onreadystatechange = this.xhrRequest.onreadystatechange;
this.xhrRequest.onreadystatechange = this.onReadyStateChange;
}
this.xhrRequest.addEventListener("load", this.onLoad, false);
this.xhrRequest.addEventListener("error", this.onError, false);
this.xhrRequest.addEventListener("abort", this.onAbort, false);
// xxxHonza: should be removed from FB 3.6
if (!SpyHttpActivityObserver.getActivityDistributor())
this.context.sourceCache.addListener(this);
},
detach: function()
{
// Bubble out if already detached.
if (!this.onLoad)
return;
// If the activity distributor is available, let's detach it when the XHR
// transaction is closed. Since, in case of multipart XHRs the onLoad method
// (readyState == 4) can be called mutliple times.
// Keep in mind:
// 1) It can happen that that the TRANSACTION_CLOSE event comes before
// the onLoad (if the XHR is made as part of the page load) so, detach if
// it's already closed.
// 2) In case of immediate cache responses, the transaction doesn't have to
// be started at all (or the activity observer is no available in Firefox 3.5).
// So, also detach in this case.
if (this.transactionStarted && !this.transactionClosed)
return;
remove(this.context.spies, this);
if (this.onreadystatechange)
this.xhrRequest.onreadystatechange = this.onreadystatechange;
try { this.xhrRequest.removeEventListener("load", this.onLoad, false); } catch (e) {}
try { this.xhrRequest.removeEventListener("error", this.onError, false); } catch (e) {}
try { this.xhrRequest.removeEventListener("abort", this.onAbort, false); } catch (e) {}
this.onreadystatechange = null;
this.onLoad = null;
this.onError = null;
this.onAbort = null;
// xxxHonza: shouuld be removed from FB 1.6
if (!SpyHttpActivityObserver.getActivityDistributor())
this.context.sourceCache.removeListener(this);
},
getURL: function()
{
return this.xhrRequest.channel ? this.xhrRequest.channel.name : this.href;
},
// Cache listener
onStopRequest: function(context, request, responseText)
{
if (!responseText)
return;
if (request == this.request)
this.responseText = responseText;
},
};
// ************************************************************************************************
function onHTTPSpyReadyStateChange(spy, event)
{
var originalHandler = spy.onreadystatechange;
// Force response text to be updated in the UI (in case the console entry
// has been already expanded and the response tab selected).
if (spy.logRow && spy.xhrRequest.readyState >= 3)
{
var netInfoBox = getChildByClass(spy.logRow, "spyHead", "netInfoBody");
if (netInfoBox)
{
netInfoBox.htmlPresented = false;
netInfoBox.responsePresented = false;
}
}
// If the request is loading update the end time.
if (spy.xhrRequest.readyState == 3)
{
spy.responseTime = spy.endTime - spy.sendTime;
updateTime(spy);
}
// Request loaded. Get all the info from the request now, just in case the
// XHR would be aborted in the original onReadyStateChange handler.
if (spy.xhrRequest.readyState == 4)
{
// Cumulate response so, multipart response content is properly displayed.
if (SpyHttpActivityObserver.getActivityDistributor())
spy.responseText += spy.xhrRequest.responseText;
else
{
// xxxHonza: remove from FB 1.6
if (!spy.responseText)
spy.responseText = spy.xhrRequest.responseText;
}
// The XHR is loaded now (used also by the activity observer).
spy.loaded = true;
// Update UI.
updateHttpSpyInfo(spy);
// Notify Net pane about a request beeing loaded.
// xxxHonza: I don't think this is necessary.
var netProgress = spy.context.netProgress;
if (netProgress)
netProgress.post(netProgress.stopFile, [spy.request, spy.endTime, spy.postText, spy.responseText]);
// Notify registered listeners about finish of the XHR.
dispatch(Firebug.Spy.fbListeners, "onLoad", [spy.context, spy]);
}
// Pass the event to the original page handler.
callPageHandler(spy, event, originalHandler);
}
function onHTTPSpyLoad(spy)
{
spy.detach();
// xxxHonza: Still needed for Fx 3.5 (#502959)
if (!SpyHttpActivityObserver.getActivityDistributor())
onHTTPSpyReadyStateChange(spy, null);
}
function onHTTPSpyError(spy)
{
spy.detach();
spy.loaded = true;
if (spy.logRow)
{
removeClass(spy.logRow, "loading");
setClass(spy.logRow, "error");
}
}
function onHTTPSpyAbort(spy)
{
spy.detach();
spy.loaded = true;
if (spy.logRow)
{
removeClass(spy.logRow, "loading");
setClass(spy.logRow, "error");
}
spy.statusText = "Aborted";
updateLogRow(spy);
// Notify Net pane about a request beeing aborted.
// xxxHonza: the net panel shoud find out this itself.
var netProgress = spy.context.netProgress;
if (netProgress)
netProgress.post(netProgress.abortFile, [spy.request, spy.endTime, spy.postText, spy.responseText]);
}
// ************************************************************************************************
function callPageHandler(spy, event, originalHandler)
{
try
{
// Calling the page handler throwed an exception (see #502959)
// This should be fixed in Firefox 3.5
if (originalHandler)
originalHandler.handleEvent(event);
}
catch (exc)
{
var error = Firebug.Errors.reparseXPC(exc, spy.context);
if (error)
{
// TODO attach trace
throw new Error(error.message, error.href, error.lineNo);
}
}
}
// ************************************************************************************************
/**
* @domplate Represents a template for XHRs logged in the Console panel. The body of the
* log (displayed when expanded) is rendered using {@link Firebug.NetMonitor.NetInfoBody}.
*/
Firebug.Spy.XHR = domplate(Firebug.Rep,
/** @lends Firebug.Spy.XHR */
{
tag:
DIV({"class": "spyHead", _repObject: "$object"},
TABLE({"class": "spyHeadTable focusRow outerFocusRow", cellpadding: 0, cellspacing: 0,
"role": "listitem", "aria-expanded": "false"},
TBODY({"role": "presentation"},
TR({"class": "spyRow"},
TD({"class": "spyTitleCol spyCol", onclick: "$onToggleBody"},
DIV({"class": "spyTitle"},
"$object|getCaption"
),
DIV({"class": "spyFullTitle spyTitle"},
"$object|getFullUri"
)
),
TD({"class": "spyCol"},
DIV({"class": "spyStatus"}, "$object|getStatus")
),
TD({"class": "spyCol"},
IMG({"class": "spyIcon", src: "blank.gif"})
),
TD({"class": "spyCol"},
SPAN({"class": "spyTime"})
),
TD({"class": "spyCol"},
TAG(FirebugReps.SourceLink.tag, {object: "$object.sourceLink"})
)
)
)
)
),
getCaption: function(spy)
{
return spy.method.toUpperCase() + " " + cropString(spy.getURL(), 100);
},
getFullUri: function(spy)
{
return spy.method.toUpperCase() + " " + spy.getURL();
},
getStatus: function(spy)
{
var text = "";
if (spy.statusCode)
text += spy.statusCode + " ";
if (spy.statusText)
return text += spy.statusText;
return text;
},
onToggleBody: function(event)
{
var target = event.currentTarget;
var logRow = getAncestorByClass(target, "logRow-spy");
if (isLeftClick(event))
{
toggleClass(logRow, "opened");
var spy = getChildByClass(logRow, "spyHead").repObject;
var spyHeadTable = getAncestorByClass(target, "spyHeadTable");
if (hasClass(logRow, "opened"))
{
updateHttpSpyInfo(spy);
if (spyHeadTable)
spyHeadTable.setAttribute('aria-expanded', 'true');
}
else
{
var netInfoBox = getChildByClass(spy.logRow, "spyHead", "netInfoBody");
dispatch(Firebug.NetMonitor.NetInfoBody.fbListeners, "destroyTabBody", [netInfoBox, spy]);
if (spyHeadTable)
spyHeadTable.setAttribute('aria-expanded', 'false');
}
}
},
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
copyURL: function(spy)
{
copyToClipboard(spy.getURL());
},
copyParams: function(spy)
{
var text = spy.postText;
if (!text)
return;
var url = reEncodeURL(spy, text, true);
copyToClipboard(url);
},
copyResponse: function(spy)
{
copyToClipboard(spy.responseText);
},
openInTab: function(spy)
{
openNewTab(spy.getURL(), spy.postText);
},
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
supportsObject: function(object)
{
return object instanceof Firebug.Spy.XMLHttpRequestSpy;
},
browseObject: function(spy, context)
{
var url = spy.getURL();
openNewTab(url);
return true;
},
getRealObject: function(spy, context)
{
return spy.xhrRequest;
},
getContextMenuItems: function(spy)
{
var items = [
{label: "CopyLocation", command: bindFixed(this.copyURL, this, spy) }
];
if (spy.postText)
{
items.push(
{label: "CopyLocationParameters", command: bindFixed(this.copyParams, this, spy) }
);
}
items.push(
{label: "CopyResponse", command: bindFixed(this.copyResponse, this, spy) },
"-",
{label: "OpenInTab", command: bindFixed(this.openInTab, this, spy) }
);
return items;
}
});
// ************************************************************************************************
Firebug.XHRSpyListener =
{
onStart: function(context, spy)
{
},
onLoad: function(context, spy)
{
}
};
// ************************************************************************************************
function updateTime(spy)
{
var timeBox = spy.logRow.getElementsByClassName("spyTime").item(0);
if (spy.responseTime)
timeBox.textContent = " " + formatTime(spy.responseTime);
}
function updateLogRow(spy)
{
updateTime(spy);
var statusBox = spy.logRow.getElementsByClassName("spyStatus").item(0);
statusBox.textContent = Firebug.Spy.XHR.getStatus(spy);
removeClass(spy.logRow, "loading");
setClass(spy.logRow, "loaded");
try
{
var errorRange = Math.floor(spy.xhrRequest.status/100);
if (errorRange == 4 || errorRange == 5)
setClass(spy.logRow, "error");
}
catch (exc)
{
}
}
function updateHttpSpyInfo(spy)
{
if (!spy.logRow || !hasClass(spy.logRow, "opened"))
return;
if (!spy.params)
spy.params = parseURLParams(spy.href+"");
if (!spy.requestHeaders)
spy.requestHeaders = getRequestHeaders(spy);
if (!spy.responseHeaders && spy.loaded)
spy.responseHeaders = getResponseHeaders(spy);
var template = Firebug.NetMonitor.NetInfoBody;
var netInfoBox = getChildByClass(spy.logRow, "spyHead", "netInfoBody");
if (!netInfoBox)
{
var head = getChildByClass(spy.logRow, "spyHead");
netInfoBox = template.tag.append({"file": spy}, head);
dispatch(template.fbListeners, "initTabBody", [netInfoBox, spy]);
template.selectTabByName(netInfoBox, "Response");
}
else
{
template.updateInfo(netInfoBox, spy, spy.context);
}
}
// ************************************************************************************************
function getRequestHeaders(spy)
{
var headers = [];
var channel = spy.xhrRequest.channel;
if (channel instanceof Ci.nsIHttpChannel)
{
channel.visitRequestHeaders({
visitHeader: function(name, value)
{
headers.push({name: name, value: value});
}
});
}
return headers;
}
function getResponseHeaders(spy)
{
var headers = [];
try
{
var channel = spy.xhrRequest.channel;
if (channel instanceof Ci.nsIHttpChannel)
{
channel.visitResponseHeaders({
visitHeader: function(name, value)
{
headers.push({name: name, value: value});
}
});
}
}
catch (exc)
{
}
return headers;
}
// ************************************************************************************************
// Tracing Listener
Firebug.Spy.TraceListener =
{
onDump: function(message)
{
var prefix = "spy.";
var index = message.text.indexOf(prefix);
if (index == 0)
{
message.text = message.text.substr(prefix.length);
message.text = trim(message.text);
message.type = "DBG_SPY";
}
}
};
// ************************************************************************************************
// Registration
Firebug.registerModule(Firebug.Spy);
Firebug.registerRep(Firebug.Spy.XHR);
// ************************************************************************************************
}});